Recalls NHTSA

O dataset utilizado apresenta um apanhado de ocorrências de recalls em veículos, disponibilizada pela National Highway Traffic Safety Administration (NHTSA).

Recall é um termo usado em diversos contextos, mas geralmente se refere a uma ação de correção ou retirada de um produto ou serviço que apresentou problemas de segurança ou eficácia. Um recall de veículos é uma ação corretiva realizada pela fabricante de um automóvel devido a problemas identificados com alguma peça ou componente do veículo. Esse tipo de recall é feito para corrigir questões de segurança ou de funcionamento do veículo antes que elas possam causar problemas para os proprietários ou para outros usuários da estrada. Em geral, o proprietário do veículo é notificado pela fabricante sobre o recall e pode levar o veículo para ser consertado em uma concessionária autorizada sem custo algum.

A NHTSA, que em português significa Administração Nacional de Segurança de Tráfego Rodoviário, é uma agência do departamento de transporte dos Estados Unidos responsável por regulamentar e garantir a segurança de veículos e equipamentos relacionados à segurança viária, incluindo regulamentação de normas para veículos, equipamentos de segurança, e a investigação de acidentes de trânsito. A NHTSA também é responsável por monitorar e realizar recalls de veículos quando necessário, a fim de garantir a segurança dos usuários da estrada.

Obtenção, estrutura e limpeza dos dados

Os dados foram obtidos diretamente do site da NHTSA. Na seção de recalls é disponibilizado o arquivo FLAT_RCL.zip que contém um arquivo .txt que foi utilizado como dataset.

Existe uma versão deste arquivo no formato csv e com os dados tratados. Porém, esta fonte de dados não será utilizada, pois não possui informações detalhadas dos modelo dos veículos.

Um dicionário de todos os dados disponibilizados está disponível em: https://static.nhtsa.gov/odi/ffdd/rcl/RCL.txt.

Code
import plotly.express as px
import plotly.graph_objects as go
import plotly.offline as pyo
import seaborn as sns
import matplotlib.pyplot as plt
from plotly.subplots import make_subplots
import numpy as np
import pandas as pd
from datetime import date
from wordcloud import WordCloud
import requests
# from google.colab import files
import seaborn as sns

pyo.init_notebook_mode()

RECALLS_DATASET_COLUMNS = ['RECORD_ID', 'CAMPNO', 'MAKETXT', 'MODELTXT', 'YEARTXT',
                           'MFGCAMPNO', 'COMPNAME', 'MFGNAME', 'BGMAN', 'ENDMAN',
                           'RCLTYPECD', 'POTAFF', 'ODATE', 'INFLUENCED_BY',
                           'MFGTXT', 'RCDATE', 'DATEA', 'RPNO', 'FMVSS','DESC_DEFECT',
                           'CONEQUENCE_DEFECT', 'CORRECTIVE_ACTION', 'NOTES',
                           'RCL_CMPT_ID', 'MFR_COMP_NAME', 'MFR_COMP_DESC', 'MFR_COMP_PTNO']

# from google.colab import drive
# drive.mount('/content/drive')

# df = pd.read_csv('/content/drive/MyDrive/Colab Notebooks/data/FLAT_RCL.txt', sep='\t', names=RECALLS_DATASET_COLUMNS, on_bad_lines='warn')

df = pd.read_csv('./FLAT_RCL.txt', sep='\t', names=RECALLS_DATASET_COLUMNS, on_bad_lines='warn')
b'Skipping line 188430: expected 27 fields, saw 34\n'
/var/folders/xd/8z39lbd16x7982mr0x9w49h80000gn/T/ipykernel_9017/1140024500.py:29: DtypeWarning:

Columns (17,18,24,25,26) have mixed types. Specify dtype option on import or set low_memory=False.

Após importado, o dataset é apresentado conforme o modelo seguinte:

Code
df.head(5)
RECORD_ID CAMPNO MAKETXT MODELTXT YEARTXT MFGCAMPNO COMPNAME MFGNAME BGMAN ENDMAN ... RPNO FMVSS DESC_DEFECT CONEQUENCE_DEFECT CORRECTIVE_ACTION NOTES RCL_CMPT_ID MFR_COMP_NAME MFR_COMP_DESC MFR_COMP_PTNO
0 1 02V288000 FORD FOCUS 2000 02S41 ELECTRICAL SYSTEM:12V/24V/48V BATTERY:CABLES FORD MOTOR COMPANY 19990719.0 20010531.0 ... NaN NaN CERTAIN PASSENGER VEHICLES EQUIPPED WITH ZETEC... THIS, IN TURN, COULD CAUSE THE BATTERY CABLES ... DEALERS WILL INSPECT THE BATTERY CABLES FOR TH... ALSO CONTACT THE NATIONAL HIGHWAY TRAFFIC SAFE... 000015339000215021000000202 NaN NaN NaN
1 2 02V288000 FORD FOCUS 2001 02S41 ELECTRICAL SYSTEM:12V/24V/48V BATTERY:CABLES FORD MOTOR COMPANY 19990719.0 20010531.0 ... NaN NaN CERTAIN PASSENGER VEHICLES EQUIPPED WITH ZETEC... THIS, IN TURN, COULD CAUSE THE BATTERY CABLES ... DEALERS WILL INSPECT THE BATTERY CABLES FOR TH... ALSO CONTACT THE NATIONAL HIGHWAY TRAFFIC SAFE... 000015339000215022000000202 NaN NaN NaN
2 3 02V236000 JAYCO FT EAGLE 10 SG 2003 NaN EQUIPMENT:OTHER:LABELS JAYCO, INC. 20020730.0 20020813.0 ... NaN NaN ON CERTAIN FOLDING TENT CAMPERS, THE FEDERAL C... IF THE TIRES WERE INFLATED TO 80 PSI, THEY COU... OWNERS WILL BE MAILED CORRECT LABELS FOR INSTA... ALSO, CUSTOMERS CAN CONTACT THE NATIONAL HIGHW... 000015210000106403000000349 NaN NaN NaN
3 4 02V237000 HOLIDAY RAMBLER ENDEAVOR 2000 NaN STRUCTURE MONACO COACH CORP. NaN NaN ... NaN NaN ON CERTAIN CLASS A MOTOR HOMES, THE FLOOR TRUS... CONDITIONS CAN RESULT IN THE BOTTOMING OUT THE... DEALERS WILL INSPECT THE FLOOR TRUSS NETWORK S... CUSTOMERS CAN ALSO CONTACT THE NATIONAL HIGHWA... 000015211000083965000000272 NaN NaN NaN
4 5 02V237000 HOLIDAY RAMBLER ENDEAVOR 1999 NaN STRUCTURE MONACO COACH CORP. NaN NaN ... NaN NaN ON CERTAIN CLASS A MOTOR HOMES, THE FLOOR TRUS... CONDITIONS CAN RESULT IN THE BOTTOMING OUT THE... DEALERS WILL INSPECT THE FLOOR TRUSS NETWORK S... CUSTOMERS CAN ALSO CONTACT THE NATIONAL HIGHWA... 000015211000080938000000272 NaN NaN NaN

5 rows × 27 columns

Renomeando e selecionando colunas

O dataset possui um grande conjunto de colunas para a descrição dos recalls, porém, essa análise será realizada apenas sobre um subconjunto dos dados do dataset. Features como descrição e correção do recall não foram consideradas nesta análise.

Assim, a análise será focada nas colunas conforme dados e tipos detalhados abaixo:

Variable Type/Size Definitions
CAMPNO CHAR(12) NHTSA CAMPAIGN NUMBER
MAKETXT CHAR(25) VEHICLE/EQUIPMENT MAKE
MODELTXT CHAR(256) VEHICLE/EQUIPMENT MODEL
YEARTXT CHAR(4) MODEL YEAR, 9999 IF UNKNOWN or N/A
COMPNAME CHAR(256) COMPONENT DESCRIPTION
RCLTYPECD CHAR(4) VEHICLE, EQUIPMENT OR TIRE REPORT
POTAFF NUMBER(9) POTENTIAL NUMBER OF UNITS AFFECTED
MFGTXT CHAR(40) MANUFACTURERS OF RECALLED VEHICLES/PRODUCTS
RCDATE CHAR(8) REPORT RECEIVED DATE
CONEQUENCE_DEFECT CHAR(2000) CONSEQUENCE SUMMARY

De forma a facilitar a compreensão e acesso aos dados, as colunas selecionadas, foram também renomeadas.

Code
RENAMED_COLUMNS = {
    'CAMPNO': 'RECALL_ID',
    'MAKETXT': 'VEHICLE_MAKE',
    'MODELTXT': 'VEHICLE_MODEL',
    'YEARTXT': 'VEHICLE_YEAR',
    'COMPNAME': 'COMPONENT_NAME',
    'RCLTYPECD': 'RECALL_TYPE_CODE',
    'POTAFF': 'AFFECTED_UNITS',
    'MFGTXT': 'MANUFACTURER',
    'RCDATE': 'REPORTED_DATE',
    'CONEQUENCE_DEFECT': 'CONSEQUENCE_SUMMARY'
}

df_raw = df
df.rename(RENAMED_COLUMNS, axis='columns', inplace=True)
Code
REQUIRED_COLUMNS = [
    'RECALL_ID',
    'VEHICLE_MAKE',
    'VEHICLE_MODEL',
    'VEHICLE_YEAR',
    'COMPONENT_NAME',
    'RECALL_TYPE_CODE',
    'AFFECTED_UNITS',
    'MANUFACTURER',
    'REPORTED_DATE'
]

df = df[REQUIRED_COLUMNS]

Após renomeadas e filtradas, o dataset é apresentado como apresentado abaixo:

Code
df.head(5)
RECALL_ID VEHICLE_MAKE VEHICLE_MODEL VEHICLE_YEAR COMPONENT_NAME RECALL_TYPE_CODE AFFECTED_UNITS MANUFACTURER REPORTED_DATE
0 02V288000 FORD FOCUS 2000 ELECTRICAL SYSTEM:12V/24V/48V BATTERY:CABLES V 291854.0 Ford Motor Company 20021106
1 02V288000 FORD FOCUS 2001 ELECTRICAL SYSTEM:12V/24V/48V BATTERY:CABLES V 291854.0 Ford Motor Company 20021106
2 02V236000 JAYCO FT EAGLE 10 SG 2003 EQUIPMENT:OTHER:LABELS V 86.0 Jayco, Inc. 20020904
3 02V237000 HOLIDAY RAMBLER ENDEAVOR 2000 STRUCTURE V 6223.0 MONACO COACH CORPORATION 20020909
4 02V237000 HOLIDAY RAMBLER ENDEAVOR 1999 STRUCTURE V 6223.0 MONACO COACH CORPORATION 20020909

Recall ID

Um recall possui um identificador único, que segue um padrão específico. Assim, a fim de seguir com dados válidos, podemos remover recalls em que o ID não segue o padrão definido pela NHTSA.

Code
VALID_RECALL_ID_PATTERN = '\d{2}[A-Z]{1}\d{2}(\d{1}|[A-Z])\d{3}'

invalid_ids = df[~df['RECALL_ID'].str.match(VALID_RECALL_ID_PATTERN)].index

df.drop(invalid_ids, axis=0, inplace=True)
/var/folders/xd/8z39lbd16x7982mr0x9w49h80000gn/T/ipykernel_9017/2931764268.py:5: SettingWithCopyWarning:


A value is trying to be set on a copy of a slice from a DataFrame

See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy

Tipos de Recalls

O dataset categoriza os recalls em alguns grupos, a partir de códigos de tipos de recall (coluna RECALL_TYPE_CODE).

Code
df['RECALL_TYPE_CODE'].unique()
array(['V', 'E', 'T', 'X', 'C', 'I'], dtype=object)
Code
df[df['RECALL_TYPE_CODE'] == 'V'][['RECALL_ID', 'RECALL_TYPE_CODE', 'VEHICLE_MAKE', 'VEHICLE_MODEL']].head(5)
RECALL_ID RECALL_TYPE_CODE VEHICLE_MAKE VEHICLE_MODEL
0 02V288000 V FORD FOCUS
1 02V288000 V FORD FOCUS
2 02V236000 V JAYCO FT EAGLE 10 SG
3 02V237000 V HOLIDAY RAMBLER ENDEAVOR
4 02V237000 V HOLIDAY RAMBLER ENDEAVOR
Code
df[df['RECALL_TYPE_CODE'] == 'I'][['RECALL_ID', 'RECALL_TYPE_CODE', 'VEHICLE_MAKE', 'VEHICLE_MODEL']].head(5)
RECALL_ID RECALL_TYPE_CODE VEHICLE_MAKE VEHICLE_MODEL
898 02I001000 I NISSAN ALTIMA
936 02I002000 I ISUZU AMIGO
937 02I002000 I ISUZU RODEO
938 02I002000 I ISUZU AMIGO
939 02I002000 I ISUZU RODEO

Recalls do tipo E, T, X e C não estão a um recall de uma marca de veículo, mas de uma marca de equipamento (E), pneu (T e X) ou assento para criança (C).

Apenas os recalls do tipo V e I serão analisados, pois ambos os tipos informam o modelo do veículo. Recalls de outros tipos serão desconsiderados.

Mesmo após filtrados, os recalls nessas categorias de componente ainda representam cerca de 90% das entradas originais do dataset.

Code
percentual_recalls_veiculos = df[df['RECALL_TYPE_CODE'].isin(['V', 'I'])]['RECALL_TYPE_CODE'].count() / df['RECALL_TYPE_CODE'].count()
print("Percentual de recalls do tipo veículo: %.2f%%"  % (percentual_recalls_veiculos * 100))
Percentual de recalls do tipo veículo: 91.70%
Code
df = df[df['RECALL_TYPE_CODE'].isin(['V', 'I'])]

Modelos de veículos

As colunas VEHICLE_MAKE e VEHICLE_MODEL apresentam, respectivamente, a montadora e modelo de veículos envolvidos no recall. Assim, as colunas foram concatenadas e utilizados para identificar um veículo no dataset.

Code
df['VEHICLE_MODEL'] = df[['VEHICLE_MAKE', 'VEHICLE_MODEL']].apply(lambda row: ' '.join(row.values.astype(str)), axis=1)
Code
df[['VEHICLE_MODEL', 'VEHICLE_YEAR']].head()
VEHICLE_MODEL VEHICLE_YEAR
0 FORD FOCUS 2000
1 FORD FOCUS 2001
2 JAYCO FT EAGLE 10 SG 2003
3 HOLIDAY RAMBLER ENDEAVOR 2000
4 HOLIDAY RAMBLER ENDEAVOR 1999

Verificamos que alguns recalls possuem marca e modelos que ainda não foram definidos. Esses casos foram removidos do dataset.

Code
df[df['VEHICLE_MODEL'].str.contains('TBD')]
RECALL_ID VEHICLE_MAKE VEHICLE_MODEL VEHICLE_YEAR COMPONENT_NAME RECALL_TYPE_CODE AFFECTED_UNITS MANUFACTURER REPORTED_DATE
742 02V229000 TBD TBD TBD 9999 EQUIPMENT:OTHER:LABELS V 550.0 Ewr Volpe Acme Inc. 20020822
4103 01V170000 TBD TBD TBD 9999 EQUIPMENT:OTHER:LABELS V 389.0 Ewr Volpe Acme Inc. 20010529
4348 01V111002 TBD TBD TBD 9999 SERVICE BRAKES, AIR:SUPPLY:COMPRESSOR V 667.0 Ewr Volpe Acme Inc. 20010626
Code
df.drop(df[df['VEHICLE_MODEL'].str.contains('TBD')].index, inplace=True)
Code
assert(df[df['VEHICLE_MODEL'].str.contains('TBD')]['VEHICLE_MODEL'].count() == 0)

Podemos verificar por anos de modelo inválidos no dataset. Alguns modelos possuem ano 9999, este valor é utilizado para quando o ano do modelo é desconhecido. Registros com ano de veículo desconhecido foram removidos.

Code
UNKNOWN_MODEL_YEAR = 9999
Code
df['VEHICLE_YEAR'].unique()
array([2000, 2001, 2003, 1999, 2002, 1997, 1998, 1996, 2006, 2004, 2005,
       1987, 1988, 1990, 1991, 1993, 1994, 1995, 1989, 1992, 1986, 2010,
       1985, 1982, 1983, 1984, 1978, 1979, 1980, 1981, 1977, 1973, 1976,
       1975, 1974, 1966, 1967, 9999, 1968, 1969, 1971, 1972, 1970, 1965,
       2007, 2008, 1959, 1960, 1964, 1963, 1962, 1961, 1958, 1957, 1956,
       1955, 1950, 1951, 1952, 1953, 1954, 1949, 2009, 2011, 2012, 2013,
       2014, 2021, 2015, 2016, 2017, 2018, 2019, 2020, 2022, 2023, 2024])
Code
df[df['VEHICLE_YEAR'] == UNKNOWN_MODEL_YEAR].head(2)
RECALL_ID VEHICLE_MAKE VEHICLE_MODEL VEHICLE_YEAR COMPONENT_NAME RECALL_TYPE_CODE AFFECTED_UNITS MANUFACTURER REPORTED_DATE
15066 95I003000 MICHELIN MICHELIN 16080R16 9999 TIRES I 1500.0 MICHELIN TIRE CORP. 19951005
15067 95I003000 MICHELIN MICHELIN HI TOUR 79X TL 9999 TIRES I 1500.0 MICHELIN TIRE CORP. 19951005
Code
df[df['VEHICLE_YEAR'] == UNKNOWN_MODEL_YEAR].shape
(577, 9)
Code
df.drop(df[df['VEHICLE_YEAR'] == UNKNOWN_MODEL_YEAR].index, inplace=True)
Code
assert(df[df['VEHICLE_YEAR'] == UNKNOWN_MODEL_YEAR]['VEHICLE_YEAR'].count() == 0)

Data de report do recall

No dataset a data do recall é apresentada como texto, sem formatação específica para ser convertida e categorizada por datas, sendo apresentado no seguinte formato:

Code
df['REPORTED_DATE'].head(5)
0    20021106
1    20021106
2    20020904
3    20020909
4    20020909
Name: REPORTED_DATE, dtype: int64

A fim de facilitar a análise dos dados baseada também nas datas dos recalls, a data em que o recall foi reportado foi ser convertida para datetime.

Code
REPORT_RECEIVED_DATE_FORMAT = '%Y%m%d'

df['REPORTED_DATE'] = pd.to_datetime(df['REPORTED_DATE'], format=REPORT_RECEIVED_DATE_FORMAT)

df['REPORTED_DATE'].head()
0   2002-11-06
1   2002-11-06
2   2002-09-04
3   2002-09-09
4   2002-09-09
Name: REPORTED_DATE, dtype: datetime64[ns]

Componente

Os componentes possuem nome separado por ‘:’ com informações adicionais sobre o componente. Podemos extrair apenas o conteúdo principal do nome do componente.

Code
df['COMPONENT_NAME'].head(5)
0    ELECTRICAL SYSTEM:12V/24V/48V BATTERY:CABLES
1    ELECTRICAL SYSTEM:12V/24V/48V BATTERY:CABLES
2                          EQUIPMENT:OTHER:LABELS
3                                       STRUCTURE
4                                       STRUCTURE
Name: COMPONENT_NAME, dtype: object
Code
df['COMPONENT_NAME'] = df['COMPONENT_NAME'].str.split(':').str[0]
Code
df['COMPONENT_NAME'].head()
0    ELECTRICAL SYSTEM
1    ELECTRICAL SYSTEM
2            EQUIPMENT
3            STRUCTURE
4            STRUCTURE
Name: COMPONENT_NAME, dtype: object

Alguns componentes possuem um nome diferente para um tipo de componente que já existe no dataset, e podem ser renomeados de forma a melhor agrupar os dados.

Code
DUPLICATED_COMPONENTS_TYPE = {'OTHER': 'UNKNOWN OR OTHER', 'TBD': 'UNKNOWN OR OTHER',
                              'Tether, Lower Anchor (on car seat or vehicle)': 'SEAT BELTS'}
Code
df['COMPONENT_NAME'] = df['COMPONENT_NAME'].replace(DUPLICATED_COMPONENTS_TYPE)

Existe uma grande variedade de componentes para os quais os recalls foram registrados. Como o nível de detalhamento era muito grande, foi optado por agrupá-los em categorias, de forma a ter um conjunto maior de recalls em um nível de detalhamento um pouco menor, facilitando a análise dos dados quando olhando para os grupos de dados.

Code
def get_component_category(component):
    if component in ['SEAT BELTS', 'AIR BAGS', 'FORWARD COLLISION AVOIDANCE', 'VEHICLE SPEED CONTROL', 'ELECTRONIC STABILITY CONTROL', 'LANE DEPARTURE']:
        return 'SECURITY'
    elif component in ['ENGINE', 'ENGINE AND ENGINE COOLING', 'POWER TRAIN', 'TRACTION CONTROL SYSTEM', 'HYBRID PROPULSION SYSTEM']:
        return 'ENGINE'
    elif component in ['SERVICE BRAKES', 'SERVICE BRAKES, ELECTRIC', 'SERVICE BRAKES, AIR', 'SERVICE BRAKES, HYDRAULIC', 'PARKING BRAKE']:
        return 'BRAKE'
    elif component in ['FUEL SYSTEM, DIESEL', 'FUEL SYSTEM, GASOLINE', 'FUEL SYSTEM, OTHER']:
        return 'FUEL'
    elif component in ['ELECTRICAL SYSTEM', 'INTERIOR LIGHTING']:
        return 'ELECTRICAL'
    elif component in ['STRUCTURE', 'SEATS', 'SUSPENSION', 'LATCHES/LOCKS/LINKAGES', 'STEERING', 'WHEELS', 'TIRES']:
        return 'STRUCTURAL'
    elif component in ['TRAILER HITCHES', 'CHILD SEAT', 'EQUIPMENT',  'COMMUNICATION', 'BACK OVER PREVENTION', 'EQUIPMENT ADAPTIVE/MOBILITY']:
        return 'ACCESSORIES'
    elif component in ['VISIBILITY', 'VISIBILITY/WIPER', 'EXTERIOR LIGHTING']:
        return 'VISIBILITY'
    elif component in ['UNKNOWN OR OTHER']:
        return 'OTHER'
    else:
        print('Component without category:', component)
        return component
Code
df['COMPONENT_CATEGORY'] = df['COMPONENT_NAME'].apply(get_component_category)
Code
df[['COMPONENT_CATEGORY', 'COMPONENT_NAME']].head(5)
COMPONENT_CATEGORY COMPONENT_NAME
0 ELECTRICAL ELECTRICAL SYSTEM
1 ELECTRICAL ELECTRICAL SYSTEM
2 ACCESSORIES EQUIPMENT
3 STRUCTURAL STRUCTURE
4 STRUCTURAL STRUCTURE

Fabricante

No dataset existem nomes do mesmo fabricante com formas de escritas diferentes (case sensitive). Para evitar duplicações, o nome dos fabricantes foi padronizado em upper case.

Code
df['MANUFACTURER'].head()
0          Ford Motor Company
1          Ford Motor Company
2                 Jayco, Inc.
3    MONACO COACH CORPORATION
4    MONACO COACH CORPORATION
Name: MANUFACTURER, dtype: object
Code
df['MANUFACTURER'] = df['MANUFACTURER'].str.upper()
Code
df['MANUFACTURER'].head()
0          FORD MOTOR COMPANY
1          FORD MOTOR COMPANY
2                 JAYCO, INC.
3    MONACO COACH CORPORATION
4    MONACO COACH CORPORATION
Name: MANUFACTURER, dtype: object

Unidades afetadas pelo recall

Alguns recalls possuem NA no valor de unidades afetadas. Podemos preencher esse valor com a mediana de unidades afetadas por componente de cada fabricante.

Dúvida: é a estratégia mais indicada?

Code
df[df['AFFECTED_UNITS'].isna()][['MANUFACTURER', 'COMPONENT_NAME', 'AFFECTED_UNITS']]
MANUFACTURER COMPONENT_NAME AFFECTED_UNITS
593 DAMON MOTOR COACH EQUIPMENT NaN
789 CRANE CARRIER COMPANY VEHICLE SPEED CONTROL NaN
900 GREAT DANE TRAILERS SERVICE BRAKES, AIR NaN
901 GREAT DANE TRAILERS SERVICE BRAKES, AIR NaN
902 GREAT DANE TRAILERS SERVICE BRAKES, AIR NaN
... ... ... ...
94490 FOREST RIVER, INC. ELECTRICAL SYSTEM NaN
94491 FOREST RIVER, INC. ELECTRICAL SYSTEM NaN
94492 FOREST RIVER, INC. ELECTRICAL SYSTEM NaN
94493 FOREST RIVER, INC. ELECTRICAL SYSTEM NaN
94494 FOREST RIVER, INC. ELECTRICAL SYSTEM NaN

220 rows × 3 columns

Code
mediana_unidades_afetadas = df[~df['AFFECTED_UNITS'].isna()][['MANUFACTURER', 'COMPONENT_NAME', 'AFFECTED_UNITS']].groupby(['MANUFACTURER', 'COMPONENT_NAME']).median()
Code
mediana_unidades_afetadas.head()
AFFECTED_UNITS
MANUFACTURER COMPONENT_NAME
4-STAR TRAILERS, INC. EQUIPMENT 110.0
SERVICE BRAKES, HYDRAULIC 227.0
SUSPENSION 4.0
ABC ENGINEERING, CO STEERING 570.0
VISIBILITY 255.0
Code
mediana_unidades_afetadas.loc['4-STAR TRAILERS, INC.', 'EQUIPMENT']['AFFECTED_UNITS']
110.0
Code
def fill_unidades_afetadas(row):
  fabricante = row['MANUFACTURER']
  componente = row['COMPONENT_NAME']
  try:
    row['AFFECTED_UNITS'] = mediana_unidades_afetadas.loc[fabricante, componente]['AFFECTED_UNITS']
  except KeyError:
    pass
  return row

df[df['AFFECTED_UNITS'].isna()] = df[df['AFFECTED_UNITS'].isna()].apply(fill_unidades_afetadas, axis=1)
Code
df[df['AFFECTED_UNITS'].isna()][['RECALL_ID', 'MANUFACTURER', 'COMPONENT_NAME', 'AFFECTED_UNITS']]
RECALL_ID MANUFACTURER COMPONENT_NAME AFFECTED_UNITS
2103 02V017000 WESTERN RECREATIONAL VEHICLES, INC. FUEL SYSTEM, OTHER NaN
2104 02V017000 WESTERN RECREATIONAL VEHICLES, INC. FUEL SYSTEM, OTHER NaN
2105 02V017000 WESTERN RECREATIONAL VEHICLES, INC. FUEL SYSTEM, OTHER NaN
2106 02V017000 WESTERN RECREATIONAL VEHICLES, INC. FUEL SYSTEM, OTHER NaN
2107 02V017000 WESTERN RECREATIONAL VEHICLES, INC. FUEL SYSTEM, OTHER NaN
2108 02V017000 WESTERN RECREATIONAL VEHICLES, INC. FUEL SYSTEM, OTHER NaN
2109 02V017000 WESTERN RECREATIONAL VEHICLES, INC. FUEL SYSTEM, OTHER NaN
2110 02V017000 WESTERN RECREATIONAL VEHICLES, INC. FUEL SYSTEM, OTHER NaN
4211 00V358011 PERFORMANCE FIRST, INC. SUSPENSION NaN
4906 01V097006 MID BUS CORPORATION SEAT BELTS NaN
4907 01V097006 MID BUS CORPORATION SEAT BELTS NaN
10556 98V010001 COACHMEN INDUSTRIES, INC. FUEL SYSTEM, OTHER NaN
10557 98V010001 COACHMEN INDUSTRIES, INC. FUEL SYSTEM, OTHER NaN
10558 98V010001 VIKING HOMES FUEL SYSTEM, OTHER NaN
55377 05V051000 H&W TRAILER EQUIPMENT NaN
73858 09V282000 RIDE-AWAY CORPORATION EQUIPMENT ADAPTIVE/MOBILITY NaN
85033 07V488000 NATIONAL R.V. INC. VEHICLE SPEED CONTROL NaN
86776 09V283000 RIDE-AWAY CORPORATION EQUIPMENT ADAPTIVE/MOBILITY NaN

Resumo da consequência

A coluna CONSEQUENCE_SUMMARY detalha as possíveis consequências do problema relatado para recall. Elas não possuem padronização quanto ao uso de letras maíusculas e minúsculas. Assim, para uma melhor análise, o texto das descrições foi padronizado com o uso de letras minúsculas.

Code
df_consequence = df_raw[['RECALL_ID','CONSEQUENCE_SUMMARY']].reset_index()
df_consequence['CONSEQUENCE_SUMMARY'] = df_consequence['CONSEQUENCE_SUMMARY'].str.lower()

df_consequence.drop_duplicates(inplace=True)
Code
df_consequence.head()
index RECALL_ID CONSEQUENCE_SUMMARY
0 0 02V288000 this, in turn, could cause the battery cables ...
1 1 02V288000 this, in turn, could cause the battery cables ...
2 2 02V236000 if the tires were inflated to 80 psi, they cou...
3 3 02V237000 conditions can result in the bottoming out the...
4 4 02V237000 conditions can result in the bottoming out the...

Removendo valores NA e duplicações

Code
df.info()
<class 'pandas.core.frame.DataFrame'>
Int64Index: 217483 entries, 0 to 237790
Data columns (total 10 columns):
 #   Column              Non-Null Count   Dtype         
---  ------              --------------   -----         
 0   RECALL_ID           217483 non-null  object        
 1   VEHICLE_MAKE        217483 non-null  object        
 2   VEHICLE_MODEL       217483 non-null  object        
 3   VEHICLE_YEAR        217483 non-null  int64         
 4   COMPONENT_NAME      217483 non-null  object        
 5   RECALL_TYPE_CODE    217483 non-null  object        
 6   AFFECTED_UNITS      217465 non-null  float64       
 7   MANUFACTURER        217483 non-null  object        
 8   REPORTED_DATE       217483 non-null  datetime64[ns]
 9   COMPONENT_CATEGORY  217483 non-null  object        
dtypes: datetime64[ns](1), float64(1), int64(1), object(7)
memory usage: 18.3+ MB
Code
df.isna().sum()
RECALL_ID              0
VEHICLE_MAKE           0
VEHICLE_MODEL          0
VEHICLE_YEAR           0
COMPONENT_NAME         0
RECALL_TYPE_CODE       0
AFFECTED_UNITS        18
MANUFACTURER           0
REPORTED_DATE          0
COMPONENT_CATEGORY     0
dtype: int64
Code
df.dropna(inplace=True)
Code
df.isna().sum()
RECALL_ID             0
VEHICLE_MAKE          0
VEHICLE_MODEL         0
VEHICLE_YEAR          0
COMPONENT_NAME        0
RECALL_TYPE_CODE      0
AFFECTED_UNITS        0
MANUFACTURER          0
REPORTED_DATE         0
COMPONENT_CATEGORY    0
dtype: int64
Code
df.drop_duplicates(inplace=True)
Code
df.info()
<class 'pandas.core.frame.DataFrame'>
Int64Index: 147762 entries, 0 to 237790
Data columns (total 10 columns):
 #   Column              Non-Null Count   Dtype         
---  ------              --------------   -----         
 0   RECALL_ID           147762 non-null  object        
 1   VEHICLE_MAKE        147762 non-null  object        
 2   VEHICLE_MODEL       147762 non-null  object        
 3   VEHICLE_YEAR        147762 non-null  int64         
 4   COMPONENT_NAME      147762 non-null  object        
 5   RECALL_TYPE_CODE    147762 non-null  object        
 6   AFFECTED_UNITS      147762 non-null  float64       
 7   MANUFACTURER        147762 non-null  object        
 8   REPORTED_DATE       147762 non-null  datetime64[ns]
 9   COMPONENT_CATEGORY  147762 non-null  object        
dtypes: datetime64[ns](1), float64(1), int64(1), object(7)
memory usage: 12.4+ MB

Validação e download do dataset

Podemos validar o dataset após a limpeza pesquisando pelas recalls no portal da NHTSA.

Code
df[df['RECALL_ID'] == '02V288000']
# https://www.nhtsa.gov/recalls?nhtsaId=02V288000
RECALL_ID VEHICLE_MAKE VEHICLE_MODEL VEHICLE_YEAR COMPONENT_NAME RECALL_TYPE_CODE AFFECTED_UNITS MANUFACTURER REPORTED_DATE COMPONENT_CATEGORY
0 02V288000 FORD FORD FOCUS 2000 ELECTRICAL SYSTEM V 291854.0 FORD MOTOR COMPANY 2002-11-06 ELECTRICAL
1 02V288000 FORD FORD FOCUS 2001 ELECTRICAL SYSTEM V 291854.0 FORD MOTOR COMPANY 2002-11-06 ELECTRICAL
Code
df[df['RECALL_ID'] == '02V236000']
#https://www.nhtsa.gov/recalls?nhtsaId=02V236000
RECALL_ID VEHICLE_MAKE VEHICLE_MODEL VEHICLE_YEAR COMPONENT_NAME RECALL_TYPE_CODE AFFECTED_UNITS MANUFACTURER REPORTED_DATE COMPONENT_CATEGORY
2 02V236000 JAYCO JAYCO FT EAGLE 10 SG 2003 EQUIPMENT V 86.0 JAYCO, INC. 2002-09-04 ACCESSORIES
Code
df[df['RECALL_ID'] == '02V237000'].groupby(['VEHICLE_MODEL']).count()['RECALL_ID']
#https://www.nhtsa.gov/recalls?nhtsaId=02V237000
VEHICLE_MODEL
HOLIDAY RAMBLER ADMIRAL       4
HOLIDAY RAMBLER ADMIRAL SE    2
HOLIDAY RAMBLER ENDEAVOR      3
HOLIDAY RAMBLER VACATIONER    4
MONACO COACH LAPALMA          5
MONACO COACH MONARCH          3
MONACO COACH MONARCH SE       2
Name: RECALL_ID, dtype: int64
Code
df.to_csv('recalls.csv') 
#files.download('recalls.csv')

Análise exploratória

Para a analise exploratória um dashboard foi implementado utilizando o Dash e está disponível em: https://recalls.eda-dash.lat.

O código-fonte se encontra no github: https://github.com/EdmilsonSantana/recalls-dashboard

A partir do dashboard desenvolvido, foi possível explorar principalmente a relação entre o número de recalls e número de veículos afetados por recalls, definindo filtros quanto ao período de tempo, fabricante e modelo para exploração.

Algumas análises e insights obtidos a partir da exploração do dashboard construído são apresentadas em seguida.

Números de ocorrências de recalls

  • Média de número de recalls e veículos afetados por recalls

  • Quais afetaram mais (muito acima da media)

  • Qual era o tipo desses recalls que estão muito acima da média

  • Quais componentes mais comuns em ocorrências de recalls

Code
from wordcloud import WordCloud
from wordcloud import ImageColorGenerator
from wordcloud import STOPWORDS

text = " ".join(str(i) for i in df_consequence['CONSEQUENCE_SUMMARY'])
stopwords = set(STOPWORDS)
wordcloud = WordCloud(width=800, height=400, stopwords=stopwords, background_color="white").generate(text)

plt.figure(figsize=(15,10))
plt.imshow(wordcloud, interpolation='bilinear')
plt.axis("off")
plt.show()

Figure 1: Consequence summary word cloud

Análise das recalls de 2022

Code
from datetime import datetime

time_range_start = '2022-01-01'
time_range_end = '2022-12-31'

date_format = '%Y-%m-%d'
start_date = datetime.strptime(time_range_start, date_format)
end_date = datetime.strptime(time_range_end, date_format)

df_2022 = df[df['REPORTED_DATE'].between(start_date, end_date)]
Code
num_recalls = df_2022['RECALL_ID'].nunique()
print(f'Number of unique recalls: {num_recalls}')

# affected_models = df[['VEHICLE_MODEL', 'VEHICLE_YEAR']].drop_duplicates().shape[0]
# print(f'Number of vehicle models: {affected_models}')

# df_affected_units = df[['RECALL_ID','AFFECTED_UNITS']].drop_duplicates()
# affected_units = df_affected_units['AFFECTED_UNITS'].sum()
# print(f'Number of affected units: {affected_units/1000000}MM')
Number of unique recalls: 932

No ano de 2022 ocorreram 932 recalls em 4113 modelos de veículos. Mais de 30 milhões de unidades foram afetadas por recalls, um aumento de aproximadamente 7% em relação a 2021.

Dashboard Initial Page

O fabricante com maior número de recalls e maior número de unidades afetadas foi a Ford Motor Company com 67 recalls e 8.7 milhões de unidades afetadas. Este valor representa cerca de 28% do número de unidades afetadas por recalls em 2022.

Aproxidamente 33% dessas unidades correspondem a apenas um único recall, que afetou o componente Powertrain de 30 modelos de veículos da Ford Motor Company. Este recall foi o que mais afetou unidades de veículos em 2022, com aproximadamente 2.9 milhões de unidades.

Code
reported_recalls_and_affected_units = df_2022[['RECALL_ID', 'REPORTED_DATE', 'AFFECTED_UNITS']].drop_duplicates()

fig = px.violin(
    reported_recalls_and_affected_units,
    y='AFFECTED_UNITS',
    labels={'AFFECTED_UNITS': 'Vehicle units'},
    color_discrete_sequence=px.colors.qualitative.T10,
    title='Number of affected vehicle units distribution'
)

fig.update_layout(yaxis_title=None, xaxis_title=None)

fig.show()

Figure 2: Number of affected vehicle units ditribution

A Mercedes Benz foi a fabricante com mais modelos de veículos que passaram por recall, foram 340 modelos em 2022. Aproximadamente 40% destes modelos, produzidos entre 2017 até 2022, foram afetados por apenas um único recall que ocorreu nos componentes de comunicação e sistema elétrico.

Code
selected_columns = ['RECALL_ID', 'MANUFACTURER', 'REPORTED_DATE',
                    'VEHICLE_MODEL', 'VEHICLE_YEAR', 'AFFECTED_UNITS']
recalls = df_2022[selected_columns].drop_duplicates()

recalls_distribution = recalls.groupby(
    ['RECALL_ID', 'MANUFACTURER', 'AFFECTED_UNITS', 'REPORTED_DATE']).count().reset_index()

fig = px.scatter(
    recalls_distribution,
    y='VEHICLE_MODEL',
    x='AFFECTED_UNITS',
    hover_name='RECALL_ID',
    hover_data=['MANUFACTURER', 'REPORTED_DATE'],
    color_discrete_sequence=px.colors.qualitative.T10,
    labels={'VEHICLE_MODEL': 'Recalled vehicles',
            'AFFECTED_UNITS': 'Vehicle units',
            'MANUFACTURER': 'Manufacturer',
            'REPORTED_DATE': 'Reported Date'},
    title='Recalls distribution by number of affected units and vehicles recalled'
)

fig.show()

Figure 3: Recalls distribution by number of affected units and vehicles recalled

No ano de 2022, em média apenas uma recall foi reportada por veículo. Porém, existem vários outliers. Alguns exemplos:

  1. Blue Bird Vision, modelo de 2023, com 11 recalls reportadas;
  2. IC Bus CE, modelo de 2023, com 10 recalls reportadas;
  3. Tesla Model X, modelo de 2021 e 2022, com 10 recalls reportadas cada.

A mediana do número de unidades de veículos afetados por recalls em 2022 é de 459 unidades, mas existem vários outliers. Agrupando por modelo de veículo, vários modelos ultrapassam 3 milhões de unidades, tais como:

  1. Ford Fusion, modelo de 2014 e 2015;
  2. Tesla Model Y, modelo de 2020, 2021 e 2022.

OBS: não é possível distinguir o número de unidades para cada modelo de veículo individualmente, apenas por recall. Portanto, este valor não representa o número exato de unidades de um determinado modelo de veículo, mas o número de unidades de veículos que foram afetados por recalls que incluem um determinado modelo.

Code
selected_columns = ['RECALL_ID', 'MANUFACTURER',
                    'VEHICLE_MODEL', 'VEHICLE_YEAR', 'AFFECTED_UNITS']
recalls = df_2022[selected_columns].drop_duplicates()

recalls['VEHICLE'] = recalls['VEHICLE_MODEL'] + ' - ' + recalls['VEHICLE_YEAR'].astype(str)

affected_units_by_vehicle = recalls.groupby(['MANUFACTURER', 'VEHICLE']).sum(numeric_only=True)
recalls_by_vehicle = recalls.groupby(['MANUFACTURER', 'VEHICLE']).count()

recalls_by_vehicle.drop(columns=['AFFECTED_UNITS'], inplace=True)

recalls_and_affected_units_by_vehicle = pd.merge(affected_units_by_vehicle, recalls_by_vehicle, left_index=True, right_index=True).reset_index()


fig = make_subplots(rows=2, cols=1)
x = recalls_and_affected_units_by_vehicle['VEHICLE']
manufacturer = recalls_and_affected_units_by_vehicle['MANUFACTURER']
recalls_mean = recalls_and_affected_units_by_vehicle['RECALL_ID'].median()
affected_units_mean = recalls_and_affected_units_by_vehicle['AFFECTED_UNITS'].median()

fig.add_trace(
    go.Scatter(
        line=dict(color=px.colors.qualitative.T10[0]),
        x=x,
        y=recalls_and_affected_units_by_vehicle['RECALL_ID'],
        mode='markers',
        name='Recalls',
        hovertemplate='<b>Recalls</b>: %{y}' +
        '<br><b>Vehicle</b>: %{x}<br>' +
        '<b>Manufacturer</b>: %{text}',
        text=manufacturer,
        showlegend=False
    ),
    row=1,
    col=1
)

fig.add_trace(
    go.Scatter(
        line=dict(color=px.colors.qualitative.T10[1]),
        x=x,
        y=recalls_and_affected_units_by_vehicle['AFFECTED_UNITS'],
        mode='markers',
        name='Vehicle units',
        hovertemplate='<b>Vehicle Units</b>: %{y:.2f}' +
        '<br><b>Vehicle</b>: %{x}<br>' +
        '<b>Manufacturer</b>: %{text}',
        text=manufacturer,
        showlegend=False
    ),
    row=2,
    col=1
)

fig.add_hline(
    y=recalls_mean,
    line_width=3,
    row=1,
    col=1,
    line_dash="dash",
    annotation_text="Recalls median")

fig.add_hline(
    y=affected_units_mean,
    line_width=3,
    row=2,
    col=1,
    line_dash="dash",
    annotation_text="Vehicle units median")

fig.update_layout(
    title_text='Number of recalls and affected vehicle units by vehicle',
)

y_titles = {'x': 'Recalls', 'x2': 'Vehicle units'}
fig.for_each_yaxis(lambda y: y.update(title=y_titles[y.anchor]))

fig.update_xaxes(showticklabels=False, categoryorder="total descending")

fig.show()

Figure 4: Number of recalls and affected units by vehicle

Análisando os componentes que passaram por recalls e comparando o número de recalls com o número de unidades afetadas, observa-se que alguns componentes apesar de possuirem um número de recalls alto, possuem um número baixo de unidades afetadas se comparado com os demais componentes.

As recalls do componente acessório / equipamento correspondem a aproximadamente 21% do número de recalls do ano de 2022, porém o número de unidades afetadas corresponde a 2% do número total de unidades afetadas. O inverso ocorre com o componente Powertrain, com 5% do número de recalls, mas 15% do número total de unidades afetadas.

Isso pode ter ocorrido devido aos recalls de equipamento não serem tão críticos e não demandarem uma urgência, por parte do proprietário, no reparo ou troca do veículo ou por serem recalls que afetam apenas um pequeno conjunto de veículos.

Code
recalls = df_2022[['RECALL_ID', 'COMPONENT_CATEGORY',
                   'COMPONENT_NAME', 'AFFECTED_UNITS']].drop_duplicates()

recalls_by_component = recalls.groupby(['COMPONENT_CATEGORY', 'COMPONENT_NAME']).count().reset_index()

affected_units_by_component = recalls.groupby(
    ['COMPONENT_CATEGORY', 'COMPONENT_NAME']).sum(numeric_only=True).reset_index()

recalls_by_component.rename({'RECALL_ID': 'VALUE'}, axis=1, inplace=True)
recalls_by_component['CHART_TYPE'] = 'A'

affected_units_by_component.rename({'AFFECTED_UNITS': 'VALUE'}, axis=1, inplace=True)
affected_units_by_component['CHART_TYPE'] = 'B'

recalls_and_affected_units_by_component = pd.concat([recalls_by_component, affected_units_by_component])

fig = px.bar(
    recalls_and_affected_units_by_component,
    color_discrete_sequence=px.colors.qualitative.T10,
    x="COMPONENT_NAME",
    y="VALUE",
    color='COMPONENT_CATEGORY',
    facet_row='CHART_TYPE',
    title='Number of recalls and affected vehicle units by component',
    labels={"COMPONENT_CATEGORY": 'Component Category',
            "COMPONENT_NAME": 'Component Name'}
)

fig.for_each_annotation(lambda a: a.update(text=''))

y_titles = {'x2': 'Recalls', 'x': 'Vehicle units'}
fig.for_each_yaxis(lambda y: y.update(title=y_titles[y.anchor]))

fig.update_yaxes(matches=None)
fig.update_layout(hovermode="x")
fig.update_traces(hovertemplate=None)

fig.show()

Figure 5: Number of recalls and affected vehicle units by component

Esses indicadores auxiliam na tomada de decisão do fabricante para onde direcionar os esforços na melhoria de qualidade de seus processos internos, afim de reduzir o número de recalls e de unidades de veículos afetados.

Code
# TODO (sugestões por ordem de prioridade)

# 1. Validar: O dashboard está exibindo informações corretas?

# 2. Criar uma seção de Análise explanatória neste notebook:
  
#     O dashboard foi desenvolvido focando na relação entre número de recalls x número de veículos afetados.

#     Quais insights podem ser criados a partir do dashboard ? Podemos focar em um ano específico e extrair insights dele. Ex: No período x, apesar de existir um número grande de recalls de componente a, o número de unidades de veículos que foram afetadas é baixo se comparado com o número de recalls de componente b.

#     A análise explanatória poderia conter imagens geradas do dashboard. (Opção de download de imagem do plotly)

# 4. Validar gráficos do dashboard. Verificar se os dados batem com a NHTSA.
#     Podemos utilizar a seguinte url para validar: https://www.nhtsa.gov/recalls?nhtsaId={recall_id}

# 5. Gerar um pdf deste notebook.